package com.zxw.cloud.gateway.filter;

import com.alibaba.fastjson.JSONObject;
import com.zxw.cloud.gateway.ab.ReactiveRequestContextHolder;
import com.zxw.cloud.gateway.props.AuthProperties;
import com.zxw.cloud.gateway.props.RouteProperties;
import com.zxw.cloud.gateway.provider.AuthProvider;
import com.zxw.cloud.gateway.provider.ResponseProvider;
import com.zxw.cloud.gateway.utils.IPUtils;
import com.zxw.cloud.gateway.utils.RSAUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URLDecoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 全局过滤器,实现
 * @author zouxw
 * <p>
 * 全局拦截器，作用所有的微服务
 * <p>
 * 1. 对请求头中参数进行处理 from 参数进行清洗
 * 2. 重写StripPrefix = 1,支持全局
 * <p>
 * 支持swagger添加X-Forwarded-Prefix header  （F SR2 已经支持，不需要自己维护）
 */
@Slf4j
@Component
@EnableConfigurationProperties({RouteProperties.class, AuthProperties.class})
public class AuthFilter implements GlobalFilter, Ordered {
    @Value("${zcloud.debug}")
    private boolean isDebug;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private AuthProperties authProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求数据
        ServerHttpRequest request = exchange.getRequest();

        Route gatewayUrl = exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        URI uri = gatewayUrl.getUri();
        String uriPath= request.getURI().toString();
        String path = request.getURI().getPath();
        String method = request.getMethodValue();
        String instance = uri.getAuthority();
        HttpHeaders headers = request.getHeaders();
        String clientIP = IPUtils.getIpAddress(request);
        log.info("*********************请求信息*******************");
        log.info("uri:{}", uri);
        log.info("clientIP:{}", clientIP);
        log.info("URIPath:{}", uriPath);
        log.info("path:{}",path);
        log.info("method:{}", method);
        log.info("instance:{}",instance);
        log.info("header:{}", headers);
        log.info("****************************************");
        if (isDebug){
            return chain.filter(exchange);
        }
        // 校验是否指定的路径
        ServerHttpResponse response = exchange.getResponse();

        // URL白名单
        if (isSkip(path) ){
            log.info("命中URL白名单");
            return chain.filter(exchange);
        }

        // IP 白名单校验
        if(isWriteIP(clientIP)){
            log.info("命中IP白名单");
            return chain.filter(exchange);
        }

        // IP黑名单校验
        if(isBackIp(clientIP)){
            log.error("命中黑名单");
            return unAuth(exchange, ResponseProvider.unAuth("抱歉，您当前无权限访问，请联系管理员"));
        }

        // 请求头
        Map<String,Object> headerMap = new HashMap<>(headers.keySet().size());
        headers.keySet().stream().forEach(headerName->{
            String headerValue = headers.getFirst(headerName);
            headerMap.put(headerName,headerValue);
        });

        ReactiveRequestContextHolder.setRequestAttributes(headerMap);
//        ServerHttpRequest host = exchange.getRequest().mutate().
//                header("version", headers.getFirst("version")).build();
//        ServerWebExchange build = exchange.mutate().request(host).build();

        // 请求参数
        Map<String, String> params = new HashMap<>();
        request.getQueryParams().forEach((key, items) -> {
            params.put(key, items.get(0));
        });

        if (!checkTimestamp(params.get("timestamp"))){
            log.error("时间戳过期");
            return unAuth(exchange, ResponseProvider.unAuth("时间戳过期"));
        }

        if (!checkNonce(params.get("accessKey"),params.get("nonce"))){
            return unAuth(exchange, ResponseProvider.unAuth("盐值无效"));
        }

        switch (method){
            case "POST":
//                return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
//                    DataBufferUtils.retain(dataBuffer);
//                    final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
//                    final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
//                        @Override
//                        public Flux<DataBuffer> getBody() {
//                            return cachedFlux;
//                        }
//                    };
//                    final ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
//                    return cacheBody(mutatedExchange, chain, params);
//                });
                return this.checkSign(params, chain, exchange);
            case "GET":
                return this.checkSign(params, chain, exchange);
        }
        return unAuth(exchange, ResponseProvider.unAuth("抱歉，您当前无权限访问，请联系管理员"));
    }

    /**
     * 执行顺序
     * @return
     */
    @Override
    public int getOrder() {
        return -100;
    }

    // 保存HttpMessageReader
    private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();

//    @Override
//    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//        ServerHttpRequest request = exchange.getRequest();
//
//        log.info("访问地址：" + request.getURI().toString());
//
//        // 请求参数上的url地址
//        Map<String, String> params = new HashMap<>();
//        request.getQueryParams().forEach((key, items) -> {
//            params.put(key, items.get(0));
//        });

//        if ("GET".equals(request.getMethodValue())) {
//            return this.checkSign(params, chain, exchange);
//        } else if ("POST".equals(request.getMethodValue())) {
//            return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
//                DataBufferUtils.retain(dataBuffer);
//                final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
//                final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
//                    @Override
//                    public Flux<DataBuffer> getBody() {
//                        return cachedFlux;
//                    }
//                };
//                final ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
//
//                return cacheBody(mutatedExchange, chain, params);
//            });
//
//        }
//        return chain.filter(exchange);
//    }

    /***
     * 验证签名
     *
     * @param params
     * @param chain
     * @param exchange
     * @return reactor.core.publisher.Mono<java.lang.Void>
     *
     * */
    private Mono<Void> checkSign(Map<String, String> params, GatewayFilterChain chain,
                                 ServerWebExchange exchange){
        log.info("校验参数集合：{}" , params);

        try {
            if (validateSign(params) == false){
                return unAuth(exchange, ResponseProvider.unAuth("抱歉，您当前无权限访问，请联系管理员"));
            }
        } catch (Exception e) {
            log.error("验签发生异常");
            return unAuth(exchange, ResponseProvider.unAuth("抱歉，您当前无权限访问，请联系管理员"));
        }
        return chain.filter(exchange);
    }

    @SuppressWarnings("unchecked")
    private Mono<Void> cacheBody(ServerWebExchange exchange, GatewayFilterChain chain, Map<String, String> params) {
        final HttpHeaders headers = exchange.getRequest().getHeaders();
        if (headers.getContentLength() == 0) {
            return chain.filter(exchange);
        }
        final ResolvableType resolvableType;
        if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(headers.getContentType())) {
            resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
        } else {
            resolvableType = ResolvableType.forClass(String.class);
        }

        return MESSAGE_READERS.stream().filter(reader -> reader.canRead(resolvableType,
                exchange.getRequest().getHeaders().getContentType())).findFirst()
                .orElseThrow(() -> new IllegalStateException("no suitable HttpMessageReader.")).readMono(resolvableType,
                        exchange.getRequest(), Collections.emptyMap()).flatMap(resolvedBody -> {
                    if (resolvedBody instanceof MultiValueMap) {
                        @SuppressWarnings("rawtypes")
                        MultiValueMap<String, Object> map = (MultiValueMap) resolvedBody;
                        map.keySet().forEach(key -> {
//                            SynchronossPartHttpMessageReader
                            Object obj = map.get(key);
                            List<Object> list = (List<Object>) obj;
                            for (Object object : list) {
                                if (object.getClass().toString().equals("class org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader$SynchronossFilePart")) {
                                    continue;
                                }
                                Field[] fields = object.getClass().getDeclaredFields();
                                try {
                                    for (Field field : fields) {
                                        field.setAccessible(true);
                                        params.put(key, field.get(object) + "");
                                    }
                                } catch (IllegalAccessException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    } else {
                        if (null != resolvedBody) {
                            String path = null;
                            try {
                                path = URLDecoder.decode(((Object) resolvedBody).toString(), "UTF-8");
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                            if (null != path) {
                                String items[] = path.split("&");
                                for (String item: items) {
                                    String subItems[] = item.split("=");
                                    if (null != subItems && subItems.length == 2) {
                                        params.put(subItems[0], subItems[1]);
                                    }
                                }
                            }
                        }

                    }
                    return this.checkSign(params, chain, exchange);
                });
    }

    /**
     * 验证签名
     *
     * @param paramMap
     * @return
     */
    private boolean validateSign(Map<String, String> paramMap) throws Exception {
        String sign = paramMap.get("sign");
        if(StringUtils.isEmpty(sign)){
            log.error("Sign is null");
            return false;
        }

        //获取AccessKey 下面这部分，可以针对用于做认证，此处暂时忽略
		String accessKey = paramMap.get("accessKey");
		String secretKey = null;
		if(StringUtils.isEmpty(accessKey)){
            log.error("AccessKey is null");
			return false;
		}else{
			// 查询当前用户的SecretKey
            String key = "Secret_PUB_" + accessKey;
            if(redisTemplate.hasKey(key)){
                secretKey = redisTemplate.opsForValue().get("Secret_PUB_"+accessKey).toString();
            }else{
                // 调用接口
                secretKey = "JJKKKKL;;;";
            }

            if(StringUtils.isEmpty(secretKey)){
                log.error("未获取到密钥");
				return false;
			}
		}

        /**
         * 上述验证通过，开始验证参数是否正确
         * 1、按照请求参数名的字母升序排列非空请求参数（包含AccessKey），使用URL键值对的格式（即key1=value1&key2=value2…）拼接成字符串stringA，这其中包含timestamp和nonce
         * 2、在stringA最后拼接上Secretkey得到字符串stringSignTemp
         * 3、对stringSignTemp进行MD5运算，并将得到的字符串所有字符转换为大写，得到sign值。
         * 4、比较计算出的sign值和用户传入的sign值是否相等
         */
        // 排序
        Set<String> keySet = paramMap.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sortQueryStringTmp = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals("sign")) {
                continue;
            }

            // 参数值为空，则不参与签名
            if (paramMap.get(k).trim().length() > 0)
                sortQueryStringTmp.append(specialUrlEncode(k)).append("=").
                        append(specialUrlEncode(paramMap.get(k).trim())).append("&");
        }

        if (sortQueryStringTmp.length() > 0 ){
            sortQueryStringTmp = sortQueryStringTmp.deleteCharAt(sortQueryStringTmp.length()-1);
        }

        try {
            return RSAUtils.verifySign( sortQueryStringTmp.toString(), sign, secretKey);
        } catch (Exception e) {
            log.error("验签发生异常");
            return false;
        }
    }

    /**
     * URL 白名单
     *
     * @param path
     * @return
     */
    private boolean isSkip(String path){
        if (StringUtils.isEmpty(path)){
            return false;
        }

//        if (authProperties == null){
//            return false;
//        }

        log.info("AuthProvider: {}",AuthProvider.getDefaultSkipUrl());
        return AuthProvider.getDefaultSkipUrl().stream().map(
                url -> url.replace(AuthProvider.TARGET, AuthProvider.REPLACEMENT)).anyMatch(path::contains)
                || authProperties.getSkipUrl().stream().map(
                        url -> url.replace(AuthProvider.TARGET, AuthProvider.REPLACEMENT)).anyMatch(path::contains);
    }

    /**
     * 白名单IP校验
     *
     * @param ip
     * @return
     */
    private boolean isWriteIP(String ip){
        String key = "ZCLOUD_WRITE_IP";
        return  redisTemplate.opsForSet().isMember(key, ip);
    }

    /**
     * 黑名单校验
     *
     * @param ip
     * @return
     */
    private boolean isBackIp(String ip){
        String key = "ZCLOUD_BLACK_IP";
        return  redisTemplate.opsForSet().isMember(key, ip);
    }

    /**
     * 校验时间戳
     * @param timestamp
     * @return
     */
    private boolean checkTimestamp(String timestamp){
        // 断时间戳和服务器时间差值在10分钟以上不予处理
        if(timestamp == null){
            log.error("未传时间戳");
            return false;
        }

        try {
            Date clientDate = new SimpleDateFormat("yyyyMMddHHmmss").parse(timestamp);
            // 时间差值过大
            if((System.currentTimeMillis() - clientDate.getTime()) <= (10 * 60 * 1000L)){
                return true;
            }
            log.error("数据可能被篡改,{}-{}",System.currentTimeMillis(),clientDate.getTime());
        } catch (ParseException e) {
            log.error("时间戳转换异常");
        }
        return false;
    }

    /**
     * 校验随机值
     * @param nonce
     * @return
     */
    private boolean checkNonce(String accessKey,String nonce){
        if(StringUtils.isAnyEmpty(accessKey,nonce)){
            log.error("Nonce is null");
        }else{//判断请求是否重复攻击
            String key_nonce = accessKey + nonce;
            if(redisTemplate.hasKey(key_nonce) == false){
                redisTemplate.opsForValue().set(key_nonce, nonce,10, TimeUnit.MINUTES);
                return true;
            }else{
                log.error("重复提交");
            }
        }
        return false;
    }
    /**
     * 特殊URL编码
     *
     * @param value
     * @return
     * @throws Exception
     */
    private String specialUrlEncode(String value) throws Exception {
        return java.net.URLEncoder.encode(value, "UTF-8").replace("+", "%20").
                replace("*", "%2A").replace("%7E", "~");
    }

    private String sign(String accessSecret, String stringToSign) throws Exception {
        javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA1");
        mac.init(new javax.crypto.spec.SecretKeySpec(accessSecret.getBytes("UTF-8"), "HmacSHA1"));
        byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
        return new sun.misc.BASE64Encoder().encode(signData);
    }

    /**
     * 返回无访问权限
     *
     * @param exchange
     * @param map
     * @return
     */
    private Mono<Void> unAuth(ServerWebExchange exchange,Map<String,Object> map){
        if (map == null){
            throw new RuntimeException("System is Error");
        }
        exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
        exchange.getResponse().getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(
                JSONObject.toJSONString(map).getBytes()))
        );
    }
}